package io.cici.cc.mybatis.plus.core;

import io.cici.cc.mybatis.lite.builder.Builder;
import io.cici.cc.mybatis.lite.builder.BuilderException;
import io.cici.cc.mybatis.lite.builder.xml.XmlMapperBuilder;
import io.cici.cc.mybatis.lite.builder.xml.XmlMapperEntityResolver;
import io.cici.cc.mybatis.lite.datasource.DataSourceFactory;
import io.cici.cc.mybatis.lite.mapping.Environment;
import io.cici.cc.mybatis.lite.parse.Node;
import io.cici.cc.mybatis.lite.parse.XPathParser;
import io.cici.cc.mybatis.lite.session.Configuration;
import io.cici.cc.mybatis.lite.transaction.TransactionFactory;
import io.cici.cc.mybatis.lite.transaction.TransactionFactoryImpl;

import javax.sql.DataSource;
import java.io.InputStream;
import java.io.Reader;
import java.util.Properties;


public class MybatisXMLConfigBuilder extends Builder {

    private final XPathParser xPathParser;
    private boolean parsed;
    private String environment;


    public MybatisXMLConfigBuilder(Reader reader, String environment, Properties props) {
        this(new XPathParser(reader, props, true, new XmlMapperEntityResolver()), environment, props);
    }


    public MybatisXMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
        this(new XPathParser(inputStream, props, true, new XmlMapperEntityResolver()), environment, props);
    }

    private MybatisXMLConfigBuilder(XPathParser xPathParser, String environment, Properties props) {
        // TODO 使用 MybatisConfiguration 而不是 Configuration
        super(new MybatisConfiguration());

        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.xPathParser = xPathParser;
    }

    public Configuration parse() {
        if (parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        parseConfiguration(xPathParser.evalNode("/configuration"));
        return configuration;
    }

    private void parseConfiguration(Node root) {
        try {
            // issue #117 read properties first
            propertiesElement(root.evalNode("properties"));


            // read it after objectFactory and objectWrapperFactory issue #631
            environmentsElement(root.evalNode("environments"));


            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }


    private void propertiesElement(Node context) throws Exception {
        if (context != null) {
            Properties defaults = context.getChildrenAsProperties();
            String resource = context.getStringAttribute("resource");
            String url = context.getStringAttribute("url");
            if (resource != null && url != null) {
                throw new BuilderException(
                        "The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
            }

            Properties vars = configuration.getVariables();
            if (vars != null) {
                defaults.putAll(vars);
            }
            xPathParser.setVariables(defaults);
            configuration.setVariables(defaults);
        }
    }


    private void environmentsElement(Node node) throws Exception {
        if (node != null) {
            if (environment == null) {
                environment = node.getStringAttribute("default");
            }
            for (Node childNode : node.getChildNodeList()) {
                String id = childNode.getStringAttribute("id");
                if (isSpecifiedEnvironment(id)) {
                    TransactionFactory txFactory = transactionManagerElement(childNode.evalNode("transactionManager"));
                    DataSourceFactory dsFactory = dataSourceElement(childNode.evalNode("dataSource"));
                    DataSource dataSource = dsFactory.getDataSource();
                    Environment.Builder environmentBuilder = new Environment.Builder(id).transactionFactory(txFactory)
                            .dataSource(dataSource);
                    configuration.setEnvironment(environmentBuilder.build());
                    break;
                }
            }
        }
    }

    private TransactionFactory transactionManagerElement(Node context) throws Exception {
        if (context != null) {
            String type = context.getStringAttribute("type");
            Properties props = context.getChildrenAsProperties();
            TransactionFactory factory = new TransactionFactoryImpl();
            factory.setProperties(props);
            return factory;
        }
        throw new BuilderException("Environment declaration requires a TransactionFactory.");
    }

    private DataSourceFactory dataSourceElement(Node context) throws Exception {
        if (context != null) {
            String type = context.getStringAttribute("type");
            Properties props = context.getChildrenAsProperties();
            DataSourceFactory factory = new DataSourceFactory() {
                @Override
                public void setProperties(Properties properties) {

                }

                @Override
                public DataSource getDataSource() {
                    return null;
                }
            };
            factory.setProperties(props);
            return factory;
        }
        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
    }


    private void mapperElement(Node node) throws Exception {
        if (node != null) {
            for (Node childNode : node.getChildNodeList()) {
                if ("package".equals(childNode.getName())) {
                    String mapperPackage = childNode.getStringAttribute("name");
                    configuration.addMappers(mapperPackage);
                } else {
                    String resource = childNode.getStringAttribute("resource");
                    String url = childNode.getStringAttribute("url");
                    String mapperClass = childNode.getStringAttribute("class");
                    if (resource != null) {

                        try (InputStream inputStream = null) {
                            XmlMapperBuilder mapperParser = new XmlMapperBuilder(inputStream, configuration, resource,
                                    configuration.getSqlIdToNodeMap());
                            mapperParser.parse();
                        }
                    } else if (mapperClass != null) {
                        Class<?> mapperInterface = Class.forName(mapperClass);
                        configuration.addMapper(mapperInterface);
                    } else {
                        throw new BuilderException(
                                "A mapper element may only specify a url, resource or class, but not more than one.");
                    }
                }
            }
        }
    }

    private boolean isSpecifiedEnvironment(String id) {
        if (environment == null) {
            throw new BuilderException("No environment specified.");
        }
        if (id == null) {
            throw new BuilderException("Environment requires an id attribute.");
        }
        return environment.equals(id);
    }
}
